#!/usr/bin/env python3

import time

from jenkins_common import *

JENKINS_STOP_BUILD_TIMEOUT = 60  # seconds

LOG_PULL_PUSH = (
    "pull request: #" if github_pr_number == github_pr_number2 else "merge branch: "
)


def stop_previous_build(job_name, build_number, pr_number):
    """We allow concurrent builds for different pull request numbers
    but for the same pull request number only one build can run. So
    when a new build starts, a previous build with the same pull
    request number needs to be stopped.

    One complication is that a pull request may be built by different
    jobs. For example, it can be built by a push triggered job, or by
    a comment triggered job. And there is no correlation between the
    build numbers of the two jobs.

    So we simple look for any running build with the same pull request
    number, which is set by the job parameter."""

    jenkins_server = jenkins.Jenkins(
        url=jenkins_rest_api_url,
        username=jenkins_rest_api_user,
        password=jenkins_rest_api_token,
    )
    # If we find and stop a previous build, loop for up to
    # JENKINS_STOP_BUILD_TIMEOUT seconds for it to abort.
    #
    # builds_found has all the old jobs found that should be stopped.
    builds_found = {}
    end_time = time.time() + JENKINS_STOP_BUILD_TIMEOUT
    while time.time() < end_time:
        running_builds = jenkins_server.get_running_builds()
        builds_still_running = {}
        for build in running_builds:
            # Skip ourselves and higher numbered builds. It's possible
            # multiple builds for the same pull request were triggered.
            # Because even though there is a rate limit, the triggered
            # builds might be running very slowly for whatever reason.
            # So a lower numbered build might get to this script first!
            if build["name"] == job_name and build["number"] >= int(build_number):
                continue
            build_info = jenkins_server.get_build_info(build["name"], build["number"])
            # Each build will have 3 parameters:
            #
            #   - GITHUB_PR_NUMBER_PULL_REQUEST  (when triggered by pull_request)
            #   - GITHUB_PR_NUMBER_ISSUE_COMMENT (when triggered by issue_comment)
            #   - GITHUB_PR_NUMBER_PUSH          (when triggered by push)
            #
            # Only one of them will be set to the correct pull request number
            # and the other two will be set to "none". We simply search for
            # pr_number in all the values in action['parameters'] and stop
            # the previous build if we find the value.
            #
            # Note that all merges will be using the 'main' pull request number
            # so there can be only one merge at a time, across all pull requests.

            for action in build_info["actions"]:
                if (
                    "_class" not in action
                    or action["_class"] != "hudson.model.ParametersAction"
                    or "parameters" not in action
                    or not action["parameters"]
                ):
                    continue

                for parameter in action["parameters"]:
                    if (
                        "_class" not in parameter
                        or parameter["_class"] != "hudson.model.StringParameterValue"
                    ):
                        continue

                    if "value" in parameter and parameter["value"] == pr_number:
                        logging.info(
                            "Stopping job %s build #%s for " + LOG_PULL_PUSH + "%s",
                            build["name"],
                            build["number"],
                            pr_number,
                        )
                        builds_found[build["number"]] = build["name"]
                        builds_still_running[build["number"]] = build["name"]
                        jenkins_server.stop_build(build["name"], build["number"])

        # If we found and tried to stop some old builds, wait 15 seconds before
        # looping back to see if they are gone. Otherwise we are done.
        if builds_still_running:
            time.sleep(15)
        else:
            break

    # After JENKINS_STOP_BUILD_TIMEOUT seconds, we still found old running
    # builds, which means our attempt to stop them failed.
    if builds_still_running:
        raise Exception(
            ("Failed to stop {} for " + LOG_PULL_PUSH + "{} in {} seconds").format(
                str(builds_still_running), pr_number, JENKINS_STOP_BUILD_TIMEOUT
            )
        )
    # Old running builds found and we successfully stopped all of them
    elif builds_found:
        logging.info(
            "All running builds %s for " + LOG_PULL_PUSH + "%s stopped",
            str(builds_found),
            pr_number,
        )
    # Otherwise, no old running builds found
    else:
        logging.info("No running builds for " + LOG_PULL_PUSH + "%s found", pr_number)

    logging.info(
        "Runninng job %s build #%s for " + LOG_PULL_PUSH + "%s",
        job_name,
        build_number,
        pr_number,
    )


def main():
    stop_previous_build(jenkins_job_name, jenkins_build_number, github_pr_number)


if __name__ == "__main__":
    main()
